package cn.akwangl.aop;

import cn.akwangl.interceptor.LoggerWrapper;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


/**
 * 记录http请求日志,会保存请求前参数以及响应参数.
 *
 * <p>
 * 注意: 在启用异步时,获取{@code  HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();}
 * 会存在线程共享数据问题,因为{@link RequestContextHolder#getRequestAttributes()}的实现用的是{@link ThreadLocal}(ThreadLocal是起到线程隔离,每个线程只能访问自己的ThreadLocal),
 * 所以这是一个现象级问题,下面链接详细讲解了为什么会发生这样问题的原因.<br/>
 * 那要怎么解决这样的问题?<br/>
 * 1. 如果你跟我一样只是获取{@link HttpServletRequest}或{@link HttpServletResponse}对象记录HTTP请求参数的话,那么在获取时只需要判断非空就可以了,在http请求时这两个对象绝对不为空,但如果在异步线程中访问一定会有空指针问题,因为底层实现用的是{@link ThreadLocal}.<br/>
 * 2. 如果你的子线程需要用到{@link HttpServletRequest}或{@link HttpServletResponse}这两个对象,只需要将它还在父线程中提前获取(备份)在需要使用方法中,手动的传参就可以解决此问题.<br/>
 * </p>
 *
 * <a href="https://www.jianshu.com/p/3a3dc1c9bf4c">使用RequestContextHolder.getRequestAttributes()的注意事项</a>
 *
 * @author youyongkun
 * @date 2022/5/17 9:08
 * @since 0.0.4
 */
@Aspect
@Component
public class HttpLogAspect {

    private static final Logger log = LoggerWrapper.getLogger(HttpLogAspect.class);

    private Map<Class, List<Collection>> collectionTypeMap = new ConcurrentHashMap(256);

    /**
     * Controller层切点
     * <p>
     * expression="execution(* com.controller.*.*(..))" 切入点为com.controller包下的类的任意方法
     * expression="execution(* com.controller..*.*(..))" 切入点为com.controller包下子包的类的任意方法
     *
     * @author youyongkun
     * @date 2022/5/17 9:09
     * @since 0.0.2
     */
    @Pointcut("execution (public * cn.akwangl.controller..*.*(..))")
    public void controllerAspect() {
    }

    /**
     * 记录Controller切点 Http 请求参数与响应.
     *
     * @param point 切点
     * @return 切点返回响应数据
     * @author youyongkun
     * @date 2022/5/17 9:25
     * @since 0.0.2
     */
    @Around("controllerAspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 开始时间
        long startTimestamp = System.currentTimeMillis();
        ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        if (servletRequestAttributes != null) {
            HttpServletRequest request = servletRequestAttributes.getRequest();
            log.info(getPreInfo(request));
        }
        // 执行方法
        Object result = point.proceed();
        // 结束时间
        long endTimestamp = System.currentTimeMillis();
        // 运行时长
        long runTime = endTimestamp - startTimestamp;
        if (servletRequestAttributes != null) {
            HttpServletResponse response = servletRequestAttributes.getResponse();
            log.info(getAfterInfo(response, result, runTime));
        }
        return result;
    }

    /**
     * 获取请求之前日志.
     *
     * @param request servlet请求实体
     * @return 请求前日志
     * @author youyongkun
     * @date 2022/7/8 21:08
     * @since 0.0.7
     */
    private String getPreInfo(HttpServletRequest request) {
        //请求路径
        String url = request.getRequestURI();
        //获取请求参数信息
        String paramData = null;
        //设置请求方法
        String method = request.getMethod();

        if (RequestMethod.GET.name().equals(method)) {
            paramData = JSONObject.toJSONString(request.getParameterMap(),
                    SerializerFeature.DisableCircularReferenceDetect,
                    SerializerFeature.WriteMapNullValue);
        } else if (RequestMethod.POST.name().equals(method)) {
            if (request instanceof ContentCachingRequestWrapper) {
                ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
                paramData = new String(requestWrapper.getContentAsByteArray());
            } else {
                paramData = "接口为上传类型，不打印参数";
            }
        }
        String clientIp = ServletUtil.getClientIP(request);
        String preInfo = String.format("请求地址:%s,请求方法:%s,方法参数:%s,客户端ip:%s", url, method, paramData, clientIp);
        return preInfo;
    }

    /**
     * 获取请求之后日志
     *
     * @param response servlet响应实体
     * @param result   响应数据
     * @param runTime  运行时长 毫秒
     * @return 请求响应日志
     * @author youyongkun
     * @date 2022/5/16 21:07
     */
    /**
     * 获取请求之后日志
     * <p>
     * 在使用 maven:lucene-core.RamUsageEstimator#sizeOfObject方法做测试时,并没有自己重写toString()性能高,其原因是自己写的toString()
     * 方法是已知的数据结构所以递归的层次是可控的,而第三方库则是不可知的直接递归调用所以效率自然没有自己写的性能高.当然第三方库也有获取该类的实际大小
     * 但是这在使用RamUsageEstimator这个类时又必须要求JVM为"Hotspot"所以干脆自己重写实现,当然自己写的类仅仅只适合当前已知的业务场景,所以在
     * 使用时请确保该递归深度判断是有效的否则当打印到日志中又是一堆字符串非常消耗性能,具体实验代码如下:
     * <pre>
     *     StopWatch stopWatch = new StopWatch("响应json生成");
     *     stopWatch.start("重写toString");
     *     String responseBody = result.toString();
     *     stopWatch.stop();
     *
     *     stopWatch.start("ObjectSizeByte");
     *     long ObjectSizeByte = RamUsageEstimator.sizeOfObject(result);
     *     log.info("ObjectSizeByte={} byte", ObjectSizeByte);
     *     stopWatch.stop();
     *     log.info(stopWatch.prettyPrint());
     * </pre>
     * <p>
     * Maven:
     * <p>
     * <dependency>
     * <groupId>org.apache.lucene</groupId>
     * <artifactId>lucene-core</artifactId>
     * <version>8.11.2</version>
     * </dependency>
     * </p>
     * 注意:lucene-core 8.x则依赖JDK8 lucene-core 9.x则依赖JDK11
     *
     * @param response servlet响应实体
     * @param result   响应数据
     * @param runTime  运行时长 毫秒
     * @return 请求响应日志
     * @author youyongkun
     * @date 2022/5/16 21:07
     */
    private String getAfterInfo(HttpServletResponse response, Object result, long runTime) {
        //获取请求错误码
        int status = response.getStatus();
        String responseBody = result.toString();
        String afterInfo = String.format("响应状态码:%s,响应json:%s,耗时:%s毫秒", status, responseBody, runTime);
        return afterInfo;
    }
}
